/*
 * Copyright (c) 2011-2012 Alexander Dubu
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * o  Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 *
 * o  Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 *
 * o  Neither the name Axil nor the names of its contributors may be used to
 *    endorse or promote products derived from this software without specific
 *    prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

package axil.definition;

import axil.api.extend.Function;
import axil.api.extend.Source;
import axil.compiler.AxilCompiler;
import axil.engine.AxilContext;
import axil.engine.Config;
import axil.engine.function.Func;
import axil.engine.function.Functor;
import axil.engine.function.Invoker;
import axil.engine.function.NativeFunc;
import axil.framework.validation.ValidationContext;
import axil.framework.xml.InputXML;
import axil.stdlib.collection.type.Range;
import axil.vm.stack.IDedArguments;

import java.io.StringReader;
import java.util.Collection;
import java.util.List;

import static axil.framework.Functions.*;


/**
 * A function definition, in XML. This is not the executable function, but the
 * complete set of metadata for the function.
 */
public class FunctionDefinition extends DefinitionWithExample
	implements IDedArguments {
	private List<Parameter> parameters;
	private ReturnValue returns;

	private transient Module parent;
	private transient Range params;
	private transient Function function;
	private transient Functor functor;
	private transient int[] ids;


	public FunctionDefinition(InputXML xml) {
		super(xml);
		parameters = xml.objects(Parameter.class, "parameters", "parameter", Range.any);
		returns = xml.object(ReturnValue.class, "returns");
		final int size = parameters.size();
		ids = new int[size];
		for (int i = 0; i < size; i++) {
			ids[i] = parameters.get(i).id();
		}
	}


	void link(Module module) {
		this.parent = module;
	}


	public List<Parameter> parameters() {
		return parameters;
	}


	/**
	 * Get the array of IDs representing the unique ID of each argument to a
	 * function. The array returned is never null but may be empty.
	 */
	public int[] ids() {
		if (ids == null) {
			throw abort("Reference to a function not yet compiled.",
			            nv("func", this.identity()));
		}
		return ids;
	}


	public boolean accepts(Collection actual) {
		if (params == null) {
			int min = 0;
			int max = 0;
			for (Parameter p : parameters) {
				switch (p.kind()) {
				case required:
					min ++;
					if (max != Range.infinity) {
						max ++;
					}
					break;

				case repeating:
					max = Range.infinity;
					break;

				case optional:
					if (max != Range.infinity) {
						max ++;
					}
					break;
				}
			}
			params = new Range(min, max);
		}
		return params.bounds(actual);
	}


	public ReturnValue returns() {
		return returns;
	}


	public Function function() {
		if (function == null) {
			if (code().isNative()) {
				function = construct(Function.class, code().classname());
			} else {
				if (functor == null) {
					throw abort(
					    "This function is script code and has not been compiled " +
					    "yet.", nv("function", identity()));
				}
				function = new Invoker(functor);
			}
		}
		return function;
	}


	/**
	 * Return the function underlying this definition as an Axil object. This
	 * allows the function itself to be used as an object.
	 */
	public Functor functor(Source source) {
		if (functor == null) {
			assert code().isNative();
			assert function != null;
			functor = new NativeFunc(source, function, ids());
		}
		return functor;
	}


	/**
	 * Validate this object in the given context. Any problems detected with the
	 * correctness or completeness of this object are placed into the context.
	 * This method descends into its owned members, validating them as well.
	 */
	public void validate(ValidationContext context) {
		super.validate(context);
		context.validate("parameters", parameters, Range.any);
		context.validate("returns", returns);
	}


	void compile(Module module, ValidationContext context) {
		if (code().isNative()) {
			function = construct(Function.class, code().classname());
		} else {
			Config config = new Config(parent).seal();
			String name = module.identity() + ':' + this.identity() + "()";
			AxilCompiler compiler = new AxilCompiler(name,
			        new StringReader(code().script()), true, config,
			        AxilContext.empty);
			functor = compiler.functor();

			/*
			 * Due to a quirk in the compiler, we have declared the parameters
			 * twice -- once in the declaration of the function, and the second
			 * time in the actual func() that defines the implementation. So,
			 * no we remap all of the parameter IDs from the functor to the actual
			 * parameters.
			 */
			Func func = (Func)functor;
			final int size = parameters.size();
			final int[] fids = func.ids();
			boolean ok = fids.length == size;
			for (int i = 0; i < size; i++) {
				func.relink(fids[i], parameters.get(i).id());
			}

			/*
			 * Now we have compiled, but we need to verify that the formal
			 * parameters declared in the XML actually match the function
			 * itself.
			 */
			String[] actual = func.symbols();
			if (ok) {
				for (int i = 0; i < size; i++) {
					Parameter p = parameters.get(i);
					if (! actual[i].equals(p.identity())) {
						ok = false;
					}
					if (func.ids()[i] != p.id()) {
						/*
						 * And a safety check to ensure that the remapping of
						 * variable references happened correctly.
						 */
						throw abort("Unmatched IDs for function and parameter.",
						            nv("parameter", p.identity()), nv("func", ids[i]),
						            nv("param", p.id()));
					}
				}
			}
			if (! ok) {
				throw abort(
				    "The formal declared parameters in the XML definition of " +
				    "the function do not match the actual parameters in the " +
				    "script implementation of the function.",
				    nv("function", this),
				    nv("declared", separated(parameters)),
				    nv("actual", separated(actual)));
			}
		}
	}


	/**
	 * Compress this object, reducing its memory footprint as much as possible.
	 * A compressed object is still fully functional for script evaluation but
	 * may no longer possess complete metadata.
	 */
	public void compress() {
		super.compress();
		returns.compress();
		for (Parameter p : parameters) {
			p.compress();
		}
	}


	/**
	 * Get a syntactic synopsis of this function in the form "func(a, b)".
	 */
	public String synopsis() {
		StringBuilder b = new StringBuilder();
		b.append(this.identity());
		b.append('(');
		boolean first = true;
		for (Parameter p : parameters) {
			if (! first) {
				b.append(", ");
			}
			b.append(p.synopsis());
			first = false;
		}
		b.append(')');
		return b.toString();
	}


	/**
	 * Return a string representation of this object. The string returned is
	 * never null.
	 */
	public String toString() {
		return parent.identity() + ':' + this.identity();
	}
}
